summaryrefslogtreecommitdiff
path: root/app/[lng]
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]')
-rw-r--r--app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx377
-rw-r--r--app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx56
2 files changed, 433 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx
new file mode 100644
index 00000000..80cf4379
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/approval-log-detail-view.tsx
@@ -0,0 +1,377 @@
+"use client";
+
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Separator } from "@/components/ui/separator";
+import { ApprovalLogDetail } from "@/lib/approval-log/service";
+import { formatDate } from "@/lib/utils";
+import { Clock, Mail, User, FileText, Shield, AlertCircle, CheckCircle, XCircle, Zap } from "lucide-react";
+
+interface ApprovalLogDetailViewProps {
+ detail: ApprovalLogDetail;
+}
+
+export function ApprovalLogDetailView({ detail }: ApprovalLogDetailViewProps) {
+ const { approvalLog, pendingAction } = detail;
+
+ // 상태 텍스트 변환
+ const getStatusText = (status: string) => {
+ const statusMap: Record<string, string> = {
+ '-3': '암호화실패',
+ '-2': '암호화중',
+ '-1': '예약상신',
+ '0': '보류',
+ '1': '진행중',
+ '2': '완결',
+ '3': '반려',
+ '4': '상신취소',
+ '5': '전결',
+ '6': '후완결'
+ };
+ return statusMap[status] || '알 수 없음';
+ };
+
+ const getStatusVariant = (status: string) => {
+ switch (status) {
+ case '2': return 'default'; // 완결
+ case '3': return 'destructive'; // 반려
+ case '4': return 'destructive'; // 상신취소
+ case '5': return 'default'; // 전결
+ case '6': return 'default'; // 후완결
+ case '1': return 'secondary'; // 진행중
+ default: return 'outline'; // 기타
+ }
+ };
+
+ const getSecurityText = (type: string) => {
+ switch (type) {
+ case 'CONFIDENTIAL_STRICT': return '극비';
+ case 'CONFIDENTIAL': return '기밀';
+ case 'PERSONAL': return '개인';
+ default: return type || '개인';
+ }
+ };
+
+ const getSecurityVariant = (type: string) => {
+ switch (type) {
+ case 'CONFIDENTIAL_STRICT': return 'destructive';
+ case 'CONFIDENTIAL': return 'secondary';
+ default: return 'outline';
+ }
+ };
+
+ // Pending Action 상태 텍스트 및 뱃지
+ const getPendingActionStatusText = (status: string) => {
+ const statusMap: Record<string, string> = {
+ 'pending': '결재 대기 중',
+ 'approved': '결재 승인됨 (실행 대기)',
+ 'executed': '실행 완료',
+ 'failed': '실행 실패',
+ 'rejected': '결재 반려됨',
+ 'cancelled': '결재 취소됨',
+ };
+ return statusMap[status] || status;
+ };
+
+ const getPendingActionStatusVariant = (status: string) => {
+ switch (status) {
+ case 'executed': return 'default';
+ case 'failed': return 'destructive';
+ case 'rejected': return 'destructive';
+ case 'cancelled': return 'destructive';
+ case 'approved': return 'secondary';
+ case 'pending': return 'outline';
+ default: return 'outline';
+ }
+ };
+
+ // 상신일시 포맷
+ const formatSbmDt = (sbmDt: string | null) => {
+ if (!sbmDt) return '-';
+ return sbmDt.replace(
+ /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/,
+ '$1-$2-$3 $4:$5:$6'
+ );
+ };
+
+ return (
+ <div className="space-y-6">
+ {/* 결재 기본 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="h-5 w-5" />
+ 결재 기본 정보
+ </CardTitle>
+ <CardDescription>결재 문서의 기본 정보입니다.</CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <FileText className="h-4 w-4" />
+ 결재 ID
+ </div>
+ <div className="font-mono text-sm bg-muted p-2 rounded">
+ {approvalLog.apInfId}
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <AlertCircle className="h-4 w-4" />
+ 상태
+ </div>
+ <div>
+ <Badge variant={getStatusVariant(approvalLog.status)}>
+ {getStatusText(approvalLog.status)}
+ </Badge>
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <User className="h-4 w-4" />
+ 사용자 ID
+ </div>
+ <div className="text-sm">{approvalLog.userId || '-'}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <Mail className="h-4 w-4" />
+ 이메일
+ </div>
+ <div className="text-sm">{approvalLog.emailAddress}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <Clock className="h-4 w-4" />
+ 상신일시
+ </div>
+ <div className="text-sm">{formatSbmDt(approvalLog.sbmDt)}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <Shield className="h-4 w-4" />
+ 보안등급
+ </div>
+ <div>
+ <Badge variant={getSecurityVariant(approvalLog.docSecuType)}>
+ {getSecurityText(approvalLog.docSecuType)}
+ </Badge>
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <Zap className="h-4 w-4" />
+ 긴급여부
+ </div>
+ <div>
+ {approvalLog.urgYn === 'Y' ? (
+ <Badge variant="destructive">긴급</Badge>
+ ) : (
+ <span className="text-sm">일반</span>
+ )}
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
+ <FileText className="h-4 w-4" />
+ 본문종류
+ </div>
+ <div className="text-sm">{approvalLog.contentsType}</div>
+ </div>
+ </div>
+
+ <Separator />
+
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">제목</div>
+ <div className="text-base font-medium">{approvalLog.subject}</div>
+ </div>
+
+ {approvalLog.opinion && (
+ <>
+ <Separator />
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">상신의견</div>
+ <div className="text-sm">{approvalLog.opinion}</div>
+ </div>
+ </>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 결재선 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <User className="h-5 w-5" />
+ 결재선 정보
+ </CardTitle>
+ <CardDescription>결재 승인 라인 정보입니다.</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="space-y-2">
+ <pre className="text-xs bg-muted p-4 rounded overflow-auto max-h-[400px]">
+ {JSON.stringify(approvalLog.aplns, null, 2)}
+ </pre>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 결재 본문 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <FileText className="h-5 w-5" />
+ 결재 본문
+ </CardTitle>
+ <CardDescription>결재 문서의 상세 내용입니다.</CardDescription>
+ </CardHeader>
+ <CardContent>
+ {approvalLog.contentsType === 'HTML' ? (
+ <div
+ className="prose prose-sm max-w-none dark:prose-invert"
+ dangerouslySetInnerHTML={{ __html: approvalLog.content }}
+ />
+ ) : (
+ <pre className="text-sm whitespace-pre-wrap bg-muted p-4 rounded">
+ {approvalLog.content}
+ </pre>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* Pending Action 정보 */}
+ {pendingAction && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <CheckCircle className="h-5 w-5" />
+ 액션 정보
+ </CardTitle>
+ <CardDescription>
+ 결재와 연결된 Pending Action 정보입니다.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">액션 ID</div>
+ <div className="font-mono text-sm">{pendingAction.id}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">액션 타입</div>
+ <div className="text-sm font-medium">{pendingAction.actionType}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">상태</div>
+ <div>
+ <Badge variant={getPendingActionStatusVariant(pendingAction.status)}>
+ {getPendingActionStatusText(pendingAction.status)}
+ </Badge>
+ </div>
+ </div>
+
+ {pendingAction.executedAt && (
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">실행 시간</div>
+ <div className="text-sm">{formatDate(pendingAction.executedAt)}</div>
+ </div>
+ )}
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">생성일</div>
+ <div className="text-sm">{formatDate(pendingAction.createdAt)}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">수정일</div>
+ <div className="text-sm">{formatDate(pendingAction.updatedAt)}</div>
+ </div>
+ </div>
+
+ <Separator />
+
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">액션 페이로드</div>
+ <pre className="text-xs bg-muted p-4 rounded overflow-auto max-h-[300px]">
+ {JSON.stringify(pendingAction.actionPayload, null, 2)}
+ </pre>
+ </div>
+
+ {pendingAction.executionResult && (
+ <>
+ <Separator />
+ <div className="space-y-2">
+ <div className="text-sm font-medium text-muted-foreground">실행 결과</div>
+ <pre className="text-xs bg-muted p-4 rounded overflow-auto max-h-[300px]">
+ {JSON.stringify(pendingAction.executionResult, null, 2)}
+ </pre>
+ </div>
+ </>
+ )}
+
+ {pendingAction.errorMessage && (
+ <>
+ <Separator />
+ <div className="space-y-2">
+ <div className="flex items-center gap-2 text-sm font-medium text-destructive">
+ <XCircle className="h-4 w-4" />
+ 에러 메시지
+ </div>
+ <div className="text-sm text-destructive bg-destructive/10 p-3 rounded">
+ {pendingAction.errorMessage}
+ </div>
+ </div>
+ </>
+ )}
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 메타데이터 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="flex items-center gap-2">
+ <Clock className="h-5 w-5" />
+ 메타데이터
+ </CardTitle>
+ <CardDescription>생성 및 수정 정보입니다.</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">생성일</div>
+ <div className="text-sm">{formatDate(approvalLog.createdAt)}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">수정일</div>
+ <div className="text-sm">{formatDate(approvalLog.updatedAt)}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">타임존</div>
+ <div className="text-sm">{approvalLog.timeZone}</div>
+ </div>
+
+ <div className="space-y-2">
+ <div className="text-sm text-muted-foreground">상신언어</div>
+ <div className="text-sm">{approvalLog.sbmLang}</div>
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
+
diff --git a/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx
new file mode 100644
index 00000000..3567d87a
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/(system)/approval/log/[apInfId]/page.tsx
@@ -0,0 +1,56 @@
+import { notFound } from "next/navigation";
+import { Shell } from "@/components/shell";
+import { getApprovalLogDetail } from "@/lib/approval-log/service";
+import { ApprovalLogDetailView } from "./approval-log-detail-view";
+import { InformationButton } from "@/components/information/information-button";
+import { Button } from "@/components/ui/button";
+import { ArrowLeft } from "lucide-react";
+import Link from "next/link";
+
+interface ApprovalLogDetailPageProps {
+ params: {
+ lng: string;
+ apInfId: string;
+ };
+}
+
+export default async function ApprovalLogDetailPage({
+ params,
+}: ApprovalLogDetailPageProps) {
+ const { lng, apInfId } = params;
+
+ // 상세 정보 조회
+ const detail = await getApprovalLogDetail(apInfId);
+
+ if (!detail) {
+ notFound();
+ }
+
+ return (
+ <Shell className="gap-4">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-4">
+ <Link href={`/${lng}/evcp/approval/log`}>
+ <Button variant="ghost" size="icon">
+ <ArrowLeft className="h-4 w-4" />
+ </Button>
+ </Link>
+ <div>
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">
+ 결재 로그 상세
+ </h2>
+ <InformationButton pagePath="evcp/approval/log" />
+ </div>
+ <p className="text-sm text-muted-foreground mt-1">
+ {detail.approvalLog.subject}
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <ApprovalLogDetailView detail={detail} />
+ </Shell>
+ );
+}
+